package com.shop.shopserver.redis;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.shop.shoppojo.entity.Goods;
import com.shop.shopserver.mapper.GoodsMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class RedisCacheOperate {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private GoodsMapper goodsMapper;

    // 设置存放缓存击标识的key-value的过期时间，设置5分钟
    private static final Long CACHE_NULL_TTL = 5L;
    // 缓存商品数据的过期时间，设置10分钟
    private static final Long CACHE_GOODS_TTL = 10L;
    // 缓存商品前缀
    private static final String CACHE_GOODS_KEY = "goods:";
    // 商品互斥锁
    private static final String LOCK_GOODS_KEY = "lock:goods:";


    /**
     * 从redis中获取缓存商品数据 解决缓存穿透问题-不存在数据设置""(防穿透标识)
     */
    public Goods getDataFromRedis(Long id) {
        String key = CACHE_GOODS_KEY + id;

        String data = redisTemplate.opsForValue().get(key);
        // 缓存命中且为有效数据
        if (StrUtil.isNotBlank(data)) {
            log.info("从redis中获取数据成功且为有效数据");
            // 存在，直接返回
            Goods goods = JSONUtil.toBean(data, Goods.class);
            // 数据存在redis
            return goods;
        }

        // 缓存命中但为空值（防穿透标识）
        if ("".equals(data)) {
            log.info("从redis中获取数据成功但为空值（防穿透标识）");
            // 调用互斥锁重建缓存 -结果1依旧为空返回设置防穿透标识并返回false -结果2存在返回查询结果
            return null;
        }

        // 缓存未命中，查询数据库，缓存重建
        log.info("从redis中获取数据失败，尝试获取互斥锁重建缓存");
        rebuildRedisCache(id);

        // 重建完成后获取redis中重建后的goods
        return getDataFromRedis(id);
    }

    /**
     * 互斥锁解决缓存击穿 重建缓存
     * @param id
     * @return
     */
    public void rebuildRedisCache(Long id) {
        String key = CACHE_GOODS_KEY + id;
        String lockKey = LOCK_GOODS_KEY + id;

        Goods goods = null;
        try {
            // 1.实现缓存重建
            // 1.1获取互斥锁
            boolean isLock = tryLock(lockKey);

            // 1.2判断是否成功
            if (!isLock) {
                log.info("重建redis缓存互斥锁获取失败");
                // 1.3失败，则休眠并重试
                Thread.sleep(50);
                // 递归
                rebuildRedisCache(id);
            }
            log.info("重建redis缓存互斥锁获取成功");

            // 1.4成功，根据id查询数据库
            goods = goodsMapper.selectById(id);

            // 2.不存在，返回错误
            if (goods == null) {
                log.info("重建redis缓存数据库中不存在该数据，设置防穿透标识成功");
                redisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return;
            }

            // 3.存在，写入redis
            log.info("重建redis缓存数据库中存在该数据，返回goods："+goods);
            redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(goods),CACHE_GOODS_TTL,TimeUnit.MINUTES);

        } catch (InterruptedException ex) {
            throw new RuntimeException(ex);
        } finally {
            // 4.释放锁
            unLock(lockKey);
        }
    }

    /**
     * 获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key) {
        // setnx 就是 setIfAbsent 如果存在
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        // 装箱是将值类型装换成引用类型的过程；拆箱就是将引用类型转换成值类型的过程
        // 不要直接返回flag，可能为null
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    private void unLock(String key) {
        redisTemplate.delete(key);
    }

}
